/*
 *       Copyright (c) 2008-2009 All rights reserved
 *               Asemantics S.r.l
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by
 *        Asemantics S.r.l."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 4. All advertising materials mentioning features or use of this software
 *    must display the following acknowledgement:
 *    This product includes software developed by Asemantics S.r.l.
 *    the Semantic Web company, Rome, London, Leiden and its contributors.
 *
 * 5. Neither the name of the company nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */


package com.asemantics.poco.persistence;

import com.asemantics.poco.query.Filter;
import com.asemantics.poco.query.FilterOp;
import com.asemantics.poco.query.Pagination;
import com.asemantics.poco.query.Query;
import com.asemantics.poco.query.SortOrder;
import com.asemantics.poco.query.Sorting;
import com.asemantics.poco.DateTime;
import com.asemantics.poco.Entry;
import com.asemantics.poco.Relationship;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Converts a {@link com.asemantics.poco.query.Query} object in a <i>JDOQL</i> query.
 *
 * @author Michele Mostarda ( michele.mostarda@gmail.com )
 */
public class QueryAdapter {

    /**
     * Bean info of {@link com.asemantics.poco.Entry} class.
     */
    private static final BeanInfo entryBeanInfo;


    static {
         try {
            entryBeanInfo = Introspector.getBeanInfo(Entry.class);
        } catch (IntrospectionException e) {
            throw new RuntimeException("Error while inspecting class " + Entry.class.getName());
        }
    }

    /**
     * List of efined parameters.
     */
    private List<Parameter> parameters = new ArrayList<Parameter>();

    /**
     * Constructor.
     */
    public QueryAdapter() {}

    /**
     * Converts a <i>query</i> in a <i>JDOQL</i> query profile able to retrieve a set of users and filter them
     * by specified query criteria.
     *
     * @param userID the user ID of the entry to be retrieved.
     * if <code>null</code> all the entries are retrieved.
     * @param query the query to be applied.
     * @param contactsOf if <code>true</code> retrieves the contacts of the given userID, otherwise the user itself.
     * @return the query profile.
     * @see {http://www.datanucleus.org/products/accessplatform_1_1/jdo/jdoql.html}
     */
    public QueryProfile generateEntryQueryJDOQL(String userID, Query query, boolean contactsOf)
    throws QueryAdapterException {
        clear();

        final String whereClause    = createWhereClause( userID, query.getFilter(), contactsOf );
        final String orderingClause = createOrdering( query.getSorting(), contactsOf );
        final String rangeClause    = createRange( query.getPagination() );

        /*
         * SELECT [UNIQUE] [<result>] [INTO <result-class>]
         *       [FROM <candidate-class> [EXCLUDE SUBCLASSES]]
         *       [WHERE <filter>]
         *       [VARIABLES <variable declarations>]
         *       [PARAMETERS <parameter declarations>]
         *       [<import declarations>]
         *       [GROUP BY <grouping>]
         *       [ORDER BY <ordering>]
         *       [RANGE <start>, <end>]
         */

        final String queryStr = String.format(
                 ( contactsOf ? "SELECT knows FROM %s " : "SELECT FROM %s " ) +
                 "%s " + // WHERE
                 "%s " + // PARAMETERS
                 "%s " + // ORDER BY
                 "%s",   // RANGE

                 contactsOf ? Relationship.class.getName() : Entry.class.getName(),
                 whereClause,
                 getParametersList(),
                 orderingClause,
                 rangeClause
         );

        return new QueryProfile( queryStr,  getParametersMap() );
    }

    /**
     * Converts a <i>query</i> in a <i>JDOQL</i> query profile able to retrieve a set of users and filter them
     * by specified query criteria.
     *
     * @param userID the user ID of the entry to be retrieved.
     * if <code>null</code> all the entries are retrieved.
     * @param query the query to be applied.
     * @return the query profile.
     * @see {http://www.datanucleus.org/products/accessplatform_1_1/jdo/jdoql.html}
     */
    public QueryProfile generateEntryQueryJDOQL(String userID, Query query)
    throws QueryAdapterException {
        return generateEntryQueryJDOQL(userID, query, false);
    }

    /**
     * Converts a <i>query</i> in a <i>JDOQL</i> query profile able to retrieve a set of users and apply on them
     * the specified filter.
     *
     * @param query
     * @return
     * @throws QueryAdapterException
     */
    public QueryProfile retrieveEntriesJDOQL(Query query) throws QueryAdapterException {
        return generateEntryQueryJDOQL(null, query);
    }

    /**
     * Adds a parameter to the query.
     *
     * @param type
     * @param name
     * @param value
     */
    private void addParameter(String type, String name, Object value) {
        parameters.add( new Parameter(type, name, value) );
    }

    /**
     * @return formatted parameters list.
     */
    private String getParametersList() {
        if(parameters.isEmpty()) {
            return "";
        }

        StringBuilder sb = new StringBuilder();
        sb.append("PARAMETERS ");
        for(Parameter parameter : parameters) {
            sb.append(parameter.type).append(" ").append(parameter.name).append(" ");
        }
        return sb.toString();
    }

    /**
     * @return map of defined parameter names and values.
     */
    private Map<String,Object> getParametersMap() {
        Map<String,Object> parametersMap = new HashMap<String,Object>();
        for(Parameter parameter : parameters) {
            parametersMap.put(parameter.name, parameter.value);
        }
        return parametersMap;
    }

    /**
     * Creates the <i>WHERE</i> clause.
     *
     * @param userID
     * @param filter
     * @param contactsOf
     * @return
     * @throws QueryAdapterException
     */
    private String createWhereClause(String userID, Filter filter, boolean contactsOf) throws QueryAdapterException {
        if( filter == null ) {
            return "";
        }

        StringBuilder sb = new StringBuilder();
        sb.append("WHERE ");

        // Adds the filter criteria by userID if present.
        if( userID != null ) {
            if( contactsOf ) {
                sb.append( "subject.id == '").append(userID).append("'");
            } else {
                sb.append( "id == '").append(userID).append("'");
            }
            if( filter.getFilterBy() != null ) {
                sb.append(" & ");
            }
        }

        if( filter.getFilterBy() != null ) {
            if( filter.getFilterOp() == null
                    ||
                ( filter.getFilterOp() != FilterOp.present && filter.getFilterValue() == null)
            ) {
                throw new QueryAdapterException(
                        "If specified filterBy then or filterOp is " + FilterOp.present +
                        " or filterOp and filterValue are needed.");
            }

            if(contactsOf) {
                sb.append("knows.");
            }
            sb.append( filter.getFilterBy() );
            sb.append(" ");
            sb.append( convertToOperator(filter.getFilterOp(), filter.getFilterValue()) );
        }

        if( filter.getFilterBy() != null && filter.getUpdatedSince() != null) {
            sb.append(" & ");
        }

        if( filter.getUpdatedSince() != null ) {
            addParameter(DateTime.class.getName(), "updatedSince", filter.getUpdatedSince() );
            if(contactsOf) {
                sb.append(" knows.updated.date >= updatedSince.date ");   // TODO: consider also the timezone.
            } else {
                sb.append(" updated.date >= updatedSince.date ");   // TODO: consider also the timezone.
            }
        }

        return sb.toString();
    }

    /**
     * Converts the operator.
     *
     * @param filterOp
     * @param filterValue
     * @return
     */
    private String convertToOperator(FilterOp filterOp, String filterValue) {
        switch (filterOp) {
            case contains:
                return ".indexOf(\"" + filterValue + "\") != -1";
            case equals:
                return "==";
            case startswith:
                return ".startsWith(\"" + filterValue + "\")";
            case present:
                return " != null ";
            default:
                throw new IllegalArgumentException("Invalid filerOp: " + filterOp);
        }
    }

    /**
     * Creates the <i>ORDER BY</i> clause.
     * @param sorting
     * @return
     * @throws QueryAdapterException
     */
    private String createOrdering(Sorting sorting, boolean contactsOf)
    throws QueryAdapterException {
        if( sorting == null ) {
            return "";
        }

        checkSortingField(sorting);

        return
                "ORDER BY " + ( contactsOf ? " knows." : "" ) + sorting.getSortBy() +
                ( sorting.getSortOrder() == SortOrder.ascending
                        ?
                  " ASCENDING" : " DESCENDING"
                );
    }

    /**
     * Creates the <i>RANGE</i> clause.
     *
     * @param pagination
     * @return
     */
    private String createRange(Pagination pagination) {
        if( pagination == null ) {
            return "";
        }

        return String.format(
            "RANGE %d, %d",
            pagination.getStartIndex(),
            pagination.getStartIndex() + pagination.getCount()
        );
    }

    /**
     * Checks the sorting criteria.
     *
     * @param sorting
     * @throws QueryAdapterException
     */
    private void checkSortingField(Sorting sorting)
    throws QueryAdapterException {
        for(PropertyDescriptor propertyDescriptor : entryBeanInfo.getPropertyDescriptors()) {
            if(propertyDescriptor.getName().equals(sorting.getSortBy())) {
                return;
            }
        }
        throw new QueryAdapterException(
                "Error in sorting criteria: sortBy value " +
                "'" + sorting.getSortBy() + "' not present in " + Entry.class.getName()
        );
    }

    /**
     * Resets the temporary data.
     */
    private void clear() {
        parameters.clear();
    }

    /**
     * Defines an internal query parameter.
     */
    class Parameter {

        /**
         * Parameter type.
         */
        private String type;

        /**
         * Parameter name.
         */
        private String name;

        /**
         * Parameter value.
         */
        private Object value;

        Parameter(String t, String n, Object v) {
            type  = t;
            name  = n;
            value = v;
        }
    }


}
